Skip to content

Conversation

@pypae
Copy link

@pypae pypae commented Apr 24, 2024

📝 Summary

This PR adds support for pydantic options as requested in the following issue: #111
In particular it implements the behavior I described in #111 (comment).

The implementation changes the signature of the callback to add all fields of the (possibly nested) pydantic models of the original callback as typer.Options. As a result the implementation is largely independent of typer and could also be used as a standalone package which could be activated with an additional decorator on the command functions:

Usage as a decorator (draft)
import pydantic
import typer
from typer_pydantic import unwrap_pydantic_models


class User(pydantic.BaseModel):
    id: int
    name: str = "Jane Doe"

@unwrap_pydantic_models
def main(num: int, user: User):
    print(num, type(num))
    print(user, type(user))


if __name__ == "__main__":
    typer.run(main)

🔀 Related PRs

☑️ To Do

  • Make linter happy
  • Describe advantages over other approaches
  • Add support for typer.Option and typer.Argument inside pydantic models
  • Add tests for nested pydantic models
  • Make sure validation errors are reported properly
  • Make pydantic dependency optional
  • Allow sequences of pydantic models. I suggest implementing indexed list options first, see the comment below.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@svlandeg svlandeg added feature New feature, enhancement or request p3 labels Apr 25, 2024
@github-actions

This comment was marked as outdated.

@github-actions
Copy link
Contributor

📝 Docs preview for commit 12be77e at: https://ab20f60a.typertiangolo.pages.dev

@pypae pypae changed the title Support for pydantic options [Draft] ✨ Support for pydantic options Apr 25, 2024
@pypae
Copy link
Author

pypae commented Apr 26, 2024

Problem

If we want to allow sequences of pydantic models, things get unreadable quite fast.

Let's look at an example of a nested pydantic model with a sequence of another model:
pets.py

from typing import Optional, List

import typer

import pydantic


class Pet(pydantic.BaseModel):
    name: str
    species: str


class Person(pydantic.BaseModel):
    name: str
    age: Optional[float] = None
    pets: List[Pet]


def main(person: Person):
    print(person, type(person))


if __name__ == "__main__":
    typer.run(main)

The script could be called like this:

$ python pets.py --person.name Jeff --person.pets.name Lassie --person.pets.species dog

If we want to add multiple pets, we can just supply --person.pets.name and person.pets.species multiple times.

$ python pets.py --person.name Jeff --person.pets.name Lassie --person.pets.species dog --person.pets.name Nala --person.pets.species cat

We don't explicitly state which pet names and species belong together and have to rely on the correct order of parameters. In my opinion this is potentially confusing for the CLI user and may lead to bugs.

Potential Solution

To make the mapping more explicit, we could allow to enable typer.Option lists to be indexed.
Like for nested pydantic models, I suggest sticking to the syntax traefik uses for lists. I.e. entrypoints.<name>.http.tls.domains[n].main

Indexed lists could be implemented independently of this PR and should work for all lists.
I suggest adding an indexed flag on typer.Option like shown in the example below.

indexed_list.py

from typing import List

import typer

def main(indexed_list: List[int] = typer.Option(..., indexed=True)):
    print(indexed_list)

if __name__ == "__main__":
    typer.run(main)

This would then produce the following help text:

$ python indexed_list.py --help
                                                                                
 Usage: indexed_list.py [OPTIONS]                                    
                                                                    
╭─ Options ────────────────────────────────────────────────────────╮
│ *  --indexed-list[n]        INTEGER   [default: None] [required] │
│    --help                             Show this message and      │
│                                       exit.                      │
╰──────────────────────────────────────────────────────────────────╯

And could be used like this:

$ python indexed_list.py --indexed-list[1] 0 --indexed-list[0] 1 --indexed-list[2] 2
[1, 0, 2]

Note how the order of the input parameters doesn't matter anymore because the indices are given explicitly.

Notes on Implementation

Implementing this might not be trivial, but I think it could be possible by forwarding unknown options as described in the click docs.

Edit: This might actually be easier using token normalization.


Do you have any opinions on this?

@itepifanio
Copy link

@pypae why limiting the pydantic models to typer.Option? It would be nice to be able to define arguments in the pydantic model as well, for example:

class User(BaseModel):
        name: Annotated[str, typer.Argument()]      
        lastname: Annotated[str, typer.Option()]            

    @app.command()
    def cmd(user: User):
        pass

My current use case requires a set of commands with a client argument, so this would allow code reuse.

If we want to allow sequences of pydantic models, things get unreadable quite fast [...] In my opinion this is potentially confusing for the CLI user and may lead to bugs

I'm not sure how many users would required deep nested commands in their CLI, and if that's the case using a toml or yaml config file may be a better way to do that. Adding a warning note at the doc should be enough for this feature.

@pypae
Copy link
Author

pypae commented Jul 19, 2024

I started working on a standalone package to support this functionality, so you can use pydantic models with typer right now: https://github.com/pypae/pydantic-typer.

It's still WIP, and for now only covers the same basic behavior as this PR.

@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Sep 1, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2025

This pull request has a merge conflict that needs to be resolved.

@svlandeg svlandeg linked an issue Nov 19, 2025 that may be closed by this pull request
Copy link
Member

@svlandeg svlandeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't reviewed this in detail yet, but just quickly a few administrative points that we'd need to address if we do want to go forward with this PR:

  • The tutorial files should be updated to use Typer() instead of typer.run, the new format since #1418
  • There should be a new markdown file docs/tutorial/parameter-types/pydantic.md explaining this and referencing the tutorial files. This file should be included in mkdocs.yml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature, enhancement or request p3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support Pydantic Models as ParamTypes

3 participants